※ 본 챕터의 내용은 다소 고급 주제를 다루고 있습니다. 필요하지 않다면 이 챕터는 스킵해도 좋습니다.

Prerequisites

본 챕터의 내용을 더 잘 이해하기 위해선 아래의 내용을 알고 오시는 것을 추천드립니다.

  • 함수 생성
  • 익명 함수(ananymous function)
  • 로컬 함수(local function)

그래픽스 객체 컨트롤

그래프를 그려 사용하다보면 아래와 같이 figure 창에 기본적으로 내장된 기능들 외에도 추가 기능을 사용하고 싶을 때가 있다.

예를 들면, 키보드에서 더하기(+) 버튼을 누르면 현재 그려진 그래프들의 선 굵기가 1씩 커지고, 빼기(-) 버튼을 누르면 선 굵기가 1씩 작아지는 등의 기능을 추가하고 싶을 수 있다. 혹은 특정 버튼을 눌렀을 때 어떤 동작이 발생한다거나, 슬라이더를 이용해 3D로 그려진 물체를 비추는 카메라 위치를 조정하는 등의 작업을 수행할 필요가 있을 수 있다.

이런 식으로 유저의 입력을 통해 객체를 컨트롤 하는 것을 대화형 방식 컨트롤 (interactive control)이라고 부른다. 이번 챕터에서는 대화형 방식으로 그래픽스 객체를 컨트롤 하는 방법에 대해 알아볼 것이다. 특히, 이 때 중요하게 사용되는 콜백(callbacks)의 개념에 대해 알아보고, 콜백을 활용한 여러가지 응용 사례들을 짚어보자.

그래픽스 객체와 콜백

콜백은 그래픽스 객체를 클릭하거나 Figure 창을 닫는 것과 같은 일부 사전 정의된 사용자 동작에 대한 응답으로 실행되는 명령이다. 예를 들어, 버튼을 클릭했을 때(사전 정의된 사용자 동작) 새로운 창이 나타나는 것(실행되는 명령)은 동작과 콜백 간의 관계를 잘 나타내주는 예시라고 할 수 있다.


보통 콜백의 동작은 함수를 이용해 표현해주므로 프로그래밍 세계에서는 단순히 “콜백”이라는 말과 함께 “콜백 함수” 라는 말을 많이 사용하게 된다. MATLAB에서 그래픽스 객체에 사용되는 콜백함수에는 최소한 아래와 같은 두 가지 입력이 사용되어야만 한다.

  • 콜백이 작동하게 되는 그래픽스 객체: 이 입력 인자를 이용해서 콜백이 작용하는 그래픽스 객체를 특정할 수 있다.
  • 이벤트 데이터 구조체: 이 입력 인자를 이용해 콜백 속성 및 그래픽스 객체와 관련된 사용자 동작과 관련된 정보에 접근할 수 있다.

어렵게 썼다고 생각할 수도 있지만, 쉽게 말해서 콜백함수의 입력 인자는 꼭 my_callback(src, event) 와 같이 “src”, “event”와 같은 입력은 최소한으로 꼭 사용해달라는 뜻이다. “src”, “event”라고 꼭 적을 필요는 없으며 원한다면 my_callback(foo, bar) 와 같이 사용해도 괜찮다. 다만, 이 때 “foo”가 그래픽스 객체를 지칭하게 되고 “bar”가 이벤트 데이터 구조체 역할을 맡게 되는 것이다. 또 만약 my_callback(varargin) 과 같이 써주면 “varargin{1}”이 그래픽스 객체 역할을 수행하고 “varargin{2}”가 이벤트 데이터 구조체 역할을 맡게 된다.

본격적으로 예시를 통해 그래픽스 객체를 다루기 위한 콜백 함수가 어떻게 사용되는지 알아보도록 하자.

예시

아래 예시에서는 “WindowKeyPressFcn” 이라는 동작이 가해졌을 때 “figureCallback” 이라는 함수에 기술된 내용이 실행(콜백)되어 키보드의 “+”가 입력되었을 때 라인 굵기가 커지고, “-“가 입력되었을 때 라인 굵기가 얇아지는 예시이다. 여기서 “WindowKeyPressFcn”과 같은 요소는 감지하기로 약속된 액션에 관한 것이며 이 변수에 콜백을 이와 같은 요소들의 리스트는 MathWorks 홈페이지의 “Figure 속성”을 들어가보면 확인할 수 있다.


function main()

figure(WindowKeyPressFcn=@figureCallback);
plot(1:10)

end

function figureCallback(src,event)
line = findobj(src,"Type","Line");
if event.Character == "+"
    disp("keyboard typed: '+'")
    line.LineWidth = line.LineWidth+1;
elseif event.Character == "-"
    disp("keyboard typed: '-'")
    line.LineWidth = max(line.LineWidth-1,0.5);
end
end

uicontrol

콜백을 활용하기에 좋은 그래픽스 객체 요소로써 버튼, 슬라이더 등이 있다. 이런 요소들을 figure 창에 삽입하기 위해선 “uicontrol” 함수를 이용하면 된다.

예시 1: PushButton

아래 예시에서는 PushButton UI를 이용하였다. 주목할 점은 콜백 함수 “button_callback”에 입력 인자로 “src”, “event”가 들어갔지만 모두 사용되지 않았다는 점이다. “src”, “event”의 두 인자들은 사용되지 않더라도 그래픽스 객체에 적용되는 콜백 함수라면 꼭 들어가야만 한다는 부분에 주목하자.

function main_using_pushbutton
figure('menub','no','Position',[400 400 300 120], 'numbertitle','off', 'resize','off');
uicontrol('style','pushbutton', 'position', [100, 10, 100, 20], 'String', 'click me!', 'Callback',@button_callback);
end

function button_callback(src, event)
msgbox('Congratulations!');
end

예시 2: Slider

아래 예시에서는 Slider UI를 이용하였으며 슬라이더의 위치에 맞게 점의 크기가 커지거나 작아지도록 콜백 함수를 설정하였다. 이 예시에서 주목해서 볼만한 점은 콜백 함수에 “src”, “event” 입력 외에도 추가 입력을 넣어주는 방법에 관한 것이라고 할 수 있다.

“uicontrol” 함수를 사용할 때 뒤의 'Callback', {@slider_callbac, h} 부분을 주목해서 보자. MathWorks 홈페이지의 UIControl 속성의 콜백 설명에 나와있는 것과 같이 함수 핸들에 추가 입력을 넣어주고 싶은 경우 첫 번째 요소는 함수 핸들인 셀형 배열이고 그 다음 요소들은 콜백 함수로 들어갈 인자를 넣어주도록 설계되어 있다.


function main_using_slider
figure('menub','no','Position',[400 400 300 120], ...
    'numbertitle','off', 'resize','off');
ax = axes('Position', [0.2, 0.3, 0.6, 0.6]);
h = plot(0,0,'o','MarkerSize', 10, 'MarkerFaceColor', lines(1), 'MarkerEdgecolor', 'none');
ax.Visible = 'off';

uicontrol('style','slider', 'position', [100, 10, 100, 20],...
    'String', 'click me!', 'min', 2, 'max', 30,'SliderStep',[0.1 1],'Value',10,...
    'Callback',{@slider_callback, h});
end

function slider_callback(src, event, h)
h.MarkerSize = src.Value;
end

addlistener

기본적으로 제공해주는 콜백 기능 혹은 “uicontrol”에서 제공하는 콜백 기능이 적용되지 않는 이벤트들에 대한 반응을 이끌어내야 한다면 “addlistener” 함수를 이용할 수 있다. “addlistener”를 이용하면 지정한 소스에서 특정 이벤트가 발생하는지 지속적으로 모니터링하다가 이벤트가 발생하는대로 콜백 함수를 구동시켜준다.

MathWorks 공식 메뉴얼에 있는 예제를 그대로 소개하자면 figure 창의 “Color” 속성이 바뀔 때 커맨드 창에 “Color Changed”라는 문구가 나오도록 하려면 아래와 같이 “addlistener”로 figure의 “Color” 속성에 대해 모니터링 하게 해둔다.

fig = figure;
addlistener(fig, 'Color', 'PostSet', @(src, event) disp('Color changed'));

그런 뒤 figure의 색깔을 아무 색깔로나 바꿔보자. 그러면 “Color changed”라는 메시지가 출력되는 것을 확인할 수 있다.

set(fig, 'color', 'yellow');


이 외에도 콜백 함수를 어떻게 설정하는지에 따라 다양한 활용이 가능하다.

예시 1: 서로 다른 figure 간의 통신

아래의 예시에서는 소스 axis의 카메라 위치를 목표 axis의 카메라 위치와 동일하게 만드는 작업을 수행한 경우이다.

clear; close all; clc;
a = -pi : pi/2 : pi;                                % Define Corners
ph = pi/4;                                          % Define Angular Orientation (‘Phase’)
x = [cos(a+ph); cos(a+ph)]/cos(ph);
y = [sin(a+ph); sin(a+ph)]/sin(ph);
z = [-ones(size(a)); ones(size(a))];
figure
surf(x, y, z, 'FaceColor','g')                      % Plot Cube
hold on
patch(x', y', z', 'r')                              % Make Cube Appear Solid
hold off
axis([ -1  1    -1  1    -1  1]*1.5)
grid on

xyzlim = axis;
axis_dest = gca;
ax2 = figure(2); hold on;
plot3([0,10],[0,0],[0,0],'Color',[1,0,0],'Tag','X-Axis');
plot3([0,0],[0,10],[0,0],'Color',[0,1,0],'Tag','Y-Axis');
plot3([0,0],[0,0],[0,10],'Color',[0,0,1],'Tag','Z-Axis');

view(-37.5,30)

axis(xyzlim)

axis_source = gca;

addlistener(axis_source, 'View', 'PostSet', @(src, evt) set(axis_dest, 'View', axis_source.View))

예시 2: 슬라이더 사용 시 콜백의 즉각적인 반응

이 예시에서는 슬라이더를 사용할 때 uicontrol에서 제공하는 콜백 기능을 사용하지 않고 “addlistener”를 이용하여 콜백 기능을 구현하였다. 이렇게 해주면 슬라이더의 바를 잡고 움직이는 중에도 콜백을 작동시킬 수 있어서 즉각적인 응답을 볼 수 있다. (무슨 말인지 이해가 안된다면 직접 아래의 예시를 “uicontrol”에서 제공하는 콜백 기능을 이용해 스크립트를 수정해서 확인해보자.)

function main_using_slider()
a = -pi : pi/2 : pi;                                % Define Corners
ph = pi/4;                                          % Define Angular Orientation (‘Phase’)
x = [cos(a+ph); cos(a+ph)]/cos(ph);
y = [sin(a+ph); sin(a+ph)]/sin(ph);
z = [-ones(size(a)); ones(size(a))];
figure
surf(x, y, z, 'FaceColor','g')                      % Plot Cube
hold on
patch(x', y', z', 'r')                              % Make Cube Appear Solid
hold off
axis([ -1  1    -1  1    -1  1]*1.5)
grid on

view(3);

% slider
set(gcf,'position',[410 150 560 500]);
figsize= get(gcf,'position');
Slider1 = uicontrol('style','slider','position',[figsize(3)-20, 20 20 figsize(4)-20],...
    'min', 30, 'max', 330,'SliderStep',[0.1 1],'Value',30);

Slider2 = uicontrol('style','slider','position',[figsize(3)*0, figsize(4)*0 560 20],...
    'min', -37.5, 'max', 360-37.5,'SliderStep',[0.1, 1],'Value',-37.5);

addlistener(Slider1, 'Value','PostSet',@callbackfn_slider1);
addlistener(Slider2, 'Value','PostSet',@callbackfn_slider2);

end

function callbackfn_slider1(~, eventdata)
[az,~]=view;
el = get(eventdata.AffectedObject, 'Value');
view([az,el]);
grid on;
end

function callbackfn_slider2(~, eventdata)
[~,el]=view;
az = get(eventdata.AffectedObject, 'Value');
view([az,el]);
end

참고자료